import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import hashlib
import threading
import queue

class DuplicateFileFinder:
    def __init__(self, root):
        self.root = root
        self.root.title("重复文件查找工具 - Windows版")
        self.root.geometry("1200x600")  # 增宽窗口以适应新布局
        
        # 初始化变量
        self.dir_path = tk.StringVar()
        self.scan_running = False  # 扫描状态标识
        self.checkbox_states = {}  # 复选框状态
        self.selected_files = set()  # 已选文件集合
        self.color_tags = {}  # 颜色标签映射
        self.color_index = 0  # 颜色索引计数器
        self.result_queue = queue.Queue()  # 线程通信队列
        
        # 创建界面组件
        self.create_widgets()

    def create_widgets(self):
        # 配置行列权重以支持网格布局
        self.root.grid_rowconfigure(2, weight=1)
        self.root.grid_columnconfigure(0, weight=1)
        self.root.grid_columnconfigure(1, weight=1)  # 新增右侧列
        
        # 目录选择区域
        dir_frame = ttk.Frame(self.root, padding=10)
        dir_frame.grid(row=0, column=0, columnspan=2, sticky='ew')
        
        ttk.Label(dir_frame, text="目标目录:").grid(row=0, column=0, padx=5)
        ttk.Entry(dir_frame, textvariable=self.dir_path, width=80).grid(row=0, column=1, padx=5)
        ttk.Button(dir_frame, text="浏览", command=self.choose_directory).grid(row=0, column=2, padx=5)

        # 操作按钮区域
        btn_frame = ttk.Frame(self.root, padding=10)
        btn_frame.grid(row=1, column=0, columnspan=2, sticky='ew')
        
        self.scan_btn = ttk.Button(btn_frame, text="开始扫描", command=self.start_scan)
        self.scan_btn.grid(row=0, column=0, padx=5)
        ttk.Button(btn_frame, text="删除选中", command=self.delete_selected_files).grid(row=0, column=1, padx=5)

        # 进度日志区域（移到右侧）
        log_frame = ttk.Frame(self.root, padding=10)
        log_frame.grid(row=2, column=1, sticky='nsew')
        
        self.log_text = tk.Text(log_frame, height=15, width=50, state=tk.DISABLED)  # 增加高度
        self.log_text.pack(fill=tk.BOTH, expand=True)
        
        # 结果显示区域（移到左侧，新增复选框列）
        self.tree = ttk.Treeview(self.root, columns=(
            'checkbox', 'path', 'name', 'size_kb', 'hash', 'similarity'),
            show='headings', selectmode='browse')  # 改为browse模式避免冲突
        
        # 定义列标题
        self.tree.heading('checkbox', text='选')
        self.tree.heading('path', text='完整路径')
        self.tree.heading('name', text='文件名')
        self.tree.heading('size_kb', text='大小(KB)')
        self.tree.heading('hash', text='MD5哈希')
        self.tree.heading('similarity', text='相似度')
        
        # 设置列宽度
        self.tree.column('checkbox', width=30, anchor='center')
        self.tree.column('path', width=370)
        self.tree.column('name', width=170)
        self.tree.column('size_kb', width=80)
        self.tree.column('hash', width=300)
        self.tree.column('similarity', width=80)
        
        self.tree.grid(row=2, column=0, sticky='nsew', padx=10, pady=10)
        
        # 配置行样式
        self.tree.tag_configure('selected', background='#FFFFCC')
        
        # 绑定复选框点击事件
        self.tree.bind('<Button-1>', self.on_checkbox_click)

    def choose_directory(self):
        """选择目标目录"""
        dir_path = filedialog.askdirectory(title="选择要扫描的目录")
        if dir_path:
            self.dir_path.set(dir_path)

    def start_scan(self):
        """启动扫描流程"""
        if self.scan_running:
            return  # 防止重复扫描
        
        target_dir = self.dir_path.get()
        if not os.path.isdir(target_dir):
            messagebox.showerror("错误", "请选择有效的目录路径")
            return

        self.scan_running = True
        self.scan_btn.config(text="扫描中...", state=tk.DISABLED)
        self.clear_tree()
        
        # 清空并启用日志框
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.insert(tk.END, "开始扫描...\n")
        self.log_text.config(state=tk.DISABLED)
        self.log_text.see(tk.END)  # 自动滚动到底部
        
        # 创建并启动扫描线程
        self.scan_thread = threading.Thread(
            target=self.scan_directory,
            args=(target_dir,),
            daemon=True
        )
        self.scan_thread.start()
        
        # 启动UI更新任务
        self.root.after(100, self.process_queue)

    def process_queue(self):
        """处理扫描结果队列"""
        while not self.result_queue.empty():
            msg_type, data = self.result_queue.get()
            if msg_type == 'log':
                self.log_text.config(state=tk.NORMAL)
                self.log_text.insert(tk.END, data + '\n')
                self.log_text.config(state=tk.DISABLED)
                self.log_text.see(tk.END)
            elif msg_type == 'result':
                self.handle_duplicate_groups(data)
        
        if self.scan_running:  # 继续监听直到扫描完成
            self.root.after(100, self.process_queue)

    def scan_directory(self, target_dir):
        """执行目录扫描（优化版：按大小分组+分段哈希）"""
        try:
            # 第一步：按文件大小初步分组
            self.result_queue.put(('log', '正在按文件大小分组...'))
            
            size_groups = {}
            for root, _, files in os.walk(target_dir):
                for filename in files:
                    file_path = os.path.join(root, filename)
                    try:
                        size = os.path.getsize(file_path)
                        if size not in size_groups:
                            size_groups[size] = []
                        size_groups[size].append(file_path)
                    except Exception as e:
                        self.result_queue.put(('log', f"警告：无法访问文件 {file_path} - {str(e)}"))
            
            self.result_queue.put(('log', f"完成大小分组，共{len(size_groups)}个大小组"))
            
            # 第二步：处理每个大小组
            hash_groups = {}
            for size, files in size_groups.items():
                if len(files) < 2:
                    continue  # 单文件组跳过
                
                # 计算哈希值（区分大小）
                self.result_queue.put(('log', f"处理{size/1024:.2f}KB文件组（{len(files)}个文件）"))
                
                for file_path in files:
                    try:
                        # 分段计算哈希
                        if size <= 10*1024*1024:  # 10MB以下直接全文件哈希
                            h = self.calculate_file_hash(file_path)
                        else:  # 大于10MB先计算前10MB
                            h = self.calculate_partial_hash(file_path, 10*1024*1024)
                            # 如果发现潜在重复（需要验证完整哈希）
                            if h in hash_groups and any(h in g for g in hash_groups.values()):
                                h = self.calculate_file_hash(file_path)  # 完整哈希验证
                        
                        if h not in hash_groups:
                            hash_groups[h] = []
                        hash_groups[h].append({
                            'path': file_path,
                            'name': os.path.basename(file_path),
                            'size': size,
                            'hash': h
                        })
                    except Exception as e:
                        self.result_queue.put(('log', f"警告：处理失败 {file_path} - {str(e)}"))
        finally:
            # 扫描完成
            self.result_queue.put(('result', hash_groups))
            
            self.scan_running = False
            self.root.after(0, lambda: self.scan_btn.config(text="开始扫描", state=tk.NORMAL))

    def handle_duplicate_groups(self, hash_groups):
        """处理完全重复的文件组（哈希相同）"""
        for h, files in hash_groups.items():
            if len(files) >= 2:
                # 为该文件组生成或获取颜色标签
                if h not in self.color_tags:
                    # 使用高对比度颜色方案
                    colors = ['#FFB3B3', '#B3FFB3', '#B3D9FF', '#E6B3FF', '#FFF3B3']
                    tag_name = f'group_{len(self.color_tags) % len(colors)}'
                    
                    # 添加亮度差异
                    if len(self.color_tags) % 2 == 0:
                        colors = ['#FF9999', '#99FF99', '#99CCFF', '#D999FF', '#FFF099']
                    
                    self.tree.tag_configure(tag_name, background=colors[len(self.color_tags) % len(colors)])
                    self.color_tags[h] = tag_name
                
                tag_name = self.color_tags[h]
                
                # 插入带复选框和颜色标记的行
                for file in files:
                    item_id = self.tree.insert('', tk.END, values=(
                        '☐',  # 默认未选中
                        file['path'],
                        file['name'],
                        f"{file['size']/1024:.2f}",
                        h,
                        "100.00%"
                    ), tags=(tag_name,))  # 应用颜色标签
                    
                    # 初始化复选框状态
                    self.checkbox_states[item_id] = False

    # 移除相似文件处理方法（当前需求聚焦完全重复检测）

    def calculate_partial_hash(self, file_path, bytes_to_read):
        """计算文件指定字节的哈希值（前bytes_to_read字节）"""
        hasher = hashlib.md5()
        try:
            with open(file_path, 'rb') as f:
                chunk = f.read(bytes_to_read)
                hasher.update(chunk)
        except Exception as e:
            return f"错误:{str(e)}"
        return hasher.hexdigest()

    def calculate_file_hash(self, file_path):
        """计算完整文件的MD5哈希值"""
        hasher = hashlib.md5()
        try:
            with open(file_path, 'rb') as f:
                while chunk := f.read(8192):
                    hasher.update(chunk)
        except Exception as e:
            return f"错误:{str(e)}"
        return hasher.hexdigest()

    def group_by_hash(self, file_list):
        """按哈希值分组"""
        groups = {}
        for file in file_list:
            h = file['hash']
            if h not in groups:
                groups[h] = []
            groups[h].append(file)
        return groups

    def calculate_similarity(self, file1, file2):
        """计算两个文件的字节级相似度"""
        try:
            size1 = os.path.getsize(file1)
            size2 = os.path.getsize(file2)
        except:
            return 0.0
        
        if size1 != size2:
            return 0.0  # 大小不同直接判定不相似
        
        total_bytes = size1
        same_bytes = 0
        
        with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
            while True:
                chunk1 = f1.read(8192)
                chunk2 = f2.read(8192)
                if not chunk1 and not chunk2:
                    break
                
                # 逐字节比较
                for b1, b2 in zip(chunk1, chunk2):
                    if b1 == b2:
                        same_bytes += 1
        
        return (same_bytes / total_bytes) * 100 if total_bytes > 0 else 0.0

    def delete_selected_files(self):
        """删除选中的文件"""
        if not self.selected_files:
            messagebox.showinfo("提示", "请先选择要删除的文件")
            return
        
        confirm = messagebox.askyesno("危险操作", f"确定要删除选中的 {len(self.selected_files)} 个文件吗？")
        if not confirm:
            return
        
        success_count = 0
        failed_count = 0
        failed_files = []
        
        for file_path in self.selected_files:
            try:
                os.remove(file_path)
                # 从树形视图中移除已删除的文件
                for item_id in self.tree.get_children():
                    if self.tree.item(item_id, 'values')[1] == file_path:
                        self.tree.delete(item_id)
                        break
                success_count += 1
            except Exception as e:
                failed_count += 1
                failed_files.append(f"{file_path} - {str(e)}")
        
        # 显示删除结果总结
        result_msg = f"成功删除 {success_count} 个文件"
        if failed_count > 0:
            result_msg += f"\n失败 {failed_count} 个文件:\n{chr(10).join(failed_files)}"
        
        messagebox.showinfo("删除结果", result_msg)
        
        # 清空选中状态
        self.selected_files.clear()
        self.checkbox_states = {}  # 重置复选框状态

    def clear_tree(self):
        """清空结果列表"""
        for item in self.tree.get_children():
            self.tree.delete(item)

    def on_checkbox_click(self, event):
        """处理复选框点击事件"""
        region = self.tree.identify_region(event.x, event.y)
        if region != 'cell':
            return
        
        column = self.tree.identify_column(event.x)
        if column != '#1':  # 只处理复选框列
            return
        
        item = self.tree.identify_row(event.y)
        if not item:
            return
        
        # 切换复选框状态
        current_state = self.checkbox_states.get(item, False)
        new_state = not current_state
        self.checkbox_states[item] = new_state
        
        # 更新显示和选中集合
        file_path = self.tree.item(item, 'values')[1]  # 获取文件路径
        values = list(self.tree.item(item, 'values'))
        values[0] = '☑' if new_state else '☐'  # 更新复选框显示
        self.tree.item(item, values=values)
        
        if new_state:
            self.selected_files.add(file_path)
        else:
            self.selected_files.discard(file_path)

if __name__ == "__main__":
    root = tk.Tk()
    app = DuplicateFileFinder(root)
    root.mainloop()